#include "File.h"
#include "Console.h"
#include "FileSystem.h"

#include <windows.h>
#include <conio.h>
#include <vector>

using namespace CEngine;

const char characterArray[10]= {'0','1','2','3','4','5','6','7','8','9'};

template<class INT>
INT stringToUnsigned(char* string);

template<class INT>
char* unsignedToString(const INT number);

struct HashedString {
    CHAR*   string;
    U64     hash;
    bool    onTable;
};

struct InsertedHash {
    HashedString hashedString;
    U32 start;
    U32 end;
};

U64 hashString(const char* string);

int main(int argc, char* argv[])
{
    if(argc != 3) {
        Console::print("Usage: stringprocess input_file output_file\n");
        return 0;
    }

    FilePath inputPath(argv[1]);
    FilePath outputPath(argv[2]);

    if(!inputPath.pathExists()) {
        Console::print("%q%s does not exist.\n%r", Console::LRED, inputPath.getFullPath());
        Console::print("Hit any key to exit.\n");
        while(true) {
            if(kbhit()) {
                break;
            }
        }
        return 1;
    }

    CHAR backupFilePath[261];
    for (U32 i = 0; i < inputPath.getLength(); ++i) {
        backupFilePath[i] = inputPath.getFullPath()[i];
    }

    for (U32 i = inputPath.getLength(); true; --i) {
        backupFilePath[i + 1] = backupFilePath[i];
        if(backupFilePath[i] == '.') {
            backupFilePath[i] = 'O';
            break;
        }
    }

    if(!CopyFile(inputPath.getFullPath(), backupFilePath, true)) {
        Console::print("%q%s could not be created.\nFile already exists?\n%r", Console::LRED, backupFilePath);
        Console::print("Hit any key to exit.\n");
        while(true) {
            if(kbhit()) {
                break;
            }
        }
        return 1;
    }

    CHAR directory[261];
    GetTempPath(261, directory);
    CHAR* prefix = "str";
    FilePath tempPath;
    CHAR tempPathPointer[261];
    GetTempFileNameA(directory, prefix, 0, tempPathPointer);
    tempPath = tempPathPointer;

    File input(inputPath, CE_FILE_OPEN_EXISTING | CE_FILE_ACCESS_READ | CE_FILE_ASCII | CE_FILE_HIDDEN);
    File output(outputPath, CE_FILE_OPEN_ALWAYS | CE_FILE_ACCESS_READ | CE_FILE_ACCESS_WRITE | CE_FILE_ASCII);
    File temp(tempPath, CE_FILE_OPEN_EXISTING | CE_FILE_ACCESS_READ | CE_FILE_ACCESS_WRITE | CE_FILE_ASCII);

    if(!input.open()) {
        Console::print("%q%s could not be opened.\n%r", Console::LRED, inputPath.getFullPath());
        Console::print("Hit any key to exit.\n");
        while(true) {
            if(kbhit()) {
                break;
            }
        }
        return 1;
    }

    if(!output.open()) {
        Console::print("%q%s could not be opened.\n%r", Console::LRED, outputPath.getFullPath());
        Console::print("Hit any key to exit.\n");
        while(true) {
            if(kbhit()) {
                break;
            }
        }
        return 1;
    }

    if(!temp.open()) {
        Console::print("%qtemp file could not be opened.\n%r", Console::LRED);
        Console::print("Hit any key to exit.\n");
        while(true) {
            if(kbhit()) {
                break;
            }
        }
        return 1;
    }

    std::vector<HashedString> hashTable;
    std::vector<InsertedHash> insertTable;

    U8 line[1025];
    U32 lineCount = 1;
    for (U32 i = 0; i < 1025; ++i) {
        line[i] = '\0';
    }

    while(true) {
        U64 bytesRead = output.read(line, 1024);
        if(bytesRead == 0) {
            break;
        }

        if(line[bytesRead - 1] != '\0') {
            Console::print("%qLine %u of %s was too long to read.\n%r", Console::LRED, lineCount, outputPath.getFileName());
            Console::print("Hit any key to exit.\n");
            while(true) {
                if(kbhit()) {
                    break;
                }
            }
            return 1;
        }

        U64 hash = 0;

        U8 temp[21];
        U8 c = 0;
        for (U8 i = 0; line[i] != 2; ++i) {
            temp[i] = line[i];
            ++c;
        }
        ++c;

        hash = stringToUnsigned<U64>((char*)temp);

        U8 string[1024];
        U32 length = 0;
        for (U32 i = 0; line[c + i] != 2; ++i) {
            string[i] = line[c + i];
            ++length;
        }

        HashedString hashedString;
        hashedString.hash = hash;
        hashedString.string = new CHAR[length + 1];
        hashedString.onTable = true;

        for (U32 i = 0; i < length; ++i) {
            hashedString.string[i] = string[i];
        }
        hashedString.string[length] = '\0';

        hashTable.push_back(hashedString);
        ++lineCount;
    }

    lineCount = 1;
    for (U32 i = 0; i < 1025; ++i) {
        line[i] = '\0';
    }

    U32 lineLength = 0;
    while(true) {
        if((lineLength = input.read(line, 1024)) == 0) {
            break;
        }

        if(line[1023] != '\0') {
            Console::print("%qLine %u of %s was too long to read.\n%r", Console::LRED, lineCount, inputPath.getFileName());
            Console::print("Hit any key to exit.\n");
            while(true) {
                if(kbhit()) {
                    break;
                }
            }
            return 1;
        }

        for (U32 i = 0; line[i] != '\0'; ++i) {
            if(line[i] == 'S' && line[i + 1] == 'I' && line[i + 2] == 'D') {
                if(line[i + 3] != '(' && line[i + 4] != '"') {
                    Console::print("%qSID on Line %u was formatted incorrectly.\n%r", Console::LYELLOW, lineCount);
                    Console::print("Hit any key to continue.\n");
                    while(true) {
                        if(kbhit()) {
                            break;
                        }
                    }
                }

                InsertedHash insert;
                insert.start = i;
                i += 5;
                CHAR string[1025];
                for (U32 i = 0; i < 1025; ++i) {
                    string[i] = '\0';
                }

                U32 length = 0;
                for (U32 c = 0; c < 1024; ++c) {
                    if(line[i + c] == '"' && line[i + c + 1] == ')') {
                        string[c] = '\0';
                        i += c + 2;
                        insert.end = i;
                        break;
                    }
                    string[c] = line[i + c];
                    ++length;
                }
                HashedString newHash;
                newHash.hash = hashString(string);
                newHash.string = new CHAR[length + 1];
                newHash.onTable = false;
                for (U32 i = 0; i < length; ++i) {
                    newHash.string[i] = string[i];
                }
                newHash.string[length] = '\0';

                insert.hashedString = newHash;

                bool matchFound = false;
                for (std::vector<HashedString>::iterator it = hashTable.begin() ; it != hashTable.end(); ++it) {
                    if(newHash.hash == it->hash) {
                        if(strcmp(newHash.string, it->string) != 0) {
                            Console::print("%qCollision detected on line %u between %s and %s\n", Console::LYELLOW, lineCount, newHash.string, *it->string);
                            Console::print("Hit any key to continue.\n");
                            while(true) {
                                if(kbhit()) {
                                    break;
                                }
                            }
                        }
                        insert.hashedString.onTable = true;
                        break;
                    }
                }
                insertTable.push_back(insert);
            }
        }

        if(!insertTable.empty()) {
            U32 writeBegin = 0;
            for (std::vector<InsertedHash>::iterator it = insertTable.begin(); it != insertTable.end() ; ++it) {
                InsertedHash insert = *it;
                CHAR* hash;
                hash = unsignedToString(insert.hashedString.hash);
                U32 length = 0;
                for (; hash[length] != '\0';) {
                    ++length;
                }

                temp.write(&line[writeBegin], insert.start - writeBegin);
                temp.write((U8*)hash, length);
                writeBegin = insert.end;

                if(!it->hashedString.onTable) {
                    U32 stringLength = 0;
                    for (; insert.hashedString.string[stringLength] != '\0';) {
                        ++stringLength;
                    }
                    U8 marker = 2;
                    output.write((U8*)hash, length);
                    output.write(&marker, 1);
                    output.write((U8*)insert.hashedString.string, stringLength);
                    marker = 3;
                    output.write(&marker, 1);
                    marker = '\n';
                    output.write(&marker, 1, true);
                }
            }
            U8 newline = '\n';
            temp.write(&line[writeBegin], lineLength - writeBegin);
            temp.write(&newline, 1, true);
        }
        else {
            U8 newline = '\n';
            temp.write(line, lineLength);
            temp.write(&newline, 1, true);
        }

        insertTable.clear();
        ++lineCount;
    }

    input.close();
    temp.close();
    output.close();

    FileSystem::move(temp, inputPath);

}

template<class INT>
INT stringToUnsigned(char* string) {
    INT temp = 0;

    int length = 0;
    for (int i = 0; string[i] != '\0' ; ++i) {
        ++length;
    }

    for (int i = 0; i < length; ++i) {
        INT decimal = 0;
        for (int c = 0; c < 10; ++c) {
            if (string[length - (i + 1)] == characterArray[c]) {
                decimal = c;
                break;
            }
        }
        for (int c = 0; c < i; ++c) {
            decimal *= 10;
        }
        temp += decimal;
    }
    return temp;
}

template<class INT>
char* unsignedToString(const INT number) {
    char buffer[39];
    INT temp = number;
    int length = 0;
    char* stringPointer;

    if (number == 0) {
        stringPointer = new char[2];
        stringPointer[0] = '0';
        stringPointer[1] = '\0';
        return stringPointer;
    }

    while (temp > 0) {
        int decimal = temp % 10;
        buffer[39 - (length + 1)] = characterArray[decimal];
        temp = temp / 10;
        ++length;
    }

    stringPointer = new char[length + 1];

    for (int i = 0; i < length; ++i) {
        stringPointer[i] = buffer[(39 - length) + i];
    }
    stringPointer[length] = '\0';

    return stringPointer;
}

U64
hashString(const char* string) {
    U64 seed = 12764787846358441471;
    U64 hash = 0;
    for(int i = 0; string[i] != '\0'; i++) {
        hash = (hash * seed) + string[i];
    }
   return hash;
}
